package org.dei.perla.utils.dataconverter;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.dei.perla.utils.dataconverter.annotations.CompositeField;
import org.dei.perla.utils.dataconverter.annotations.FixedLengthArray;
import org.dei.perla.utils.dataconverter.annotations.SimpleField;
import org.dei.perla.utils.dataconverter.annotations.StructInfo;
import org.dei.perla.utils.dataconverter.annotations.VariableLengthArray;
import org.dei.perla.utils.dataconverter.enums.Endianness;
import org.dei.perla.utils.dataconverter.enums.PaddingPosition;

public class DataConverter {
	
	/**
	 * Esegue la conversione da array di Byte ad oggetto. I Byte in eccesso presenti nell'array sono
	 * restituiti dalla funzione
	 * @param converted Oggetto in cui inserire i dati presenti nell'array toConvertByteArray 
	 * @param toConvertByteArray Array di Byte da caricare nell'oggetto converted passato come parametro
	 * @return Array di Byte non utilizzati durante la procedura
	 * @throws IllegalArgumentException nel caso l'oggetto passato come parametro non sia opportunamente annotato
	 * oppure l'array di Byte non contenga abbastanza elementi
	 */
	public static Byte[] fromByteArray(Object converted, Byte[] toConvertByteArray) throws IllegalArgumentException {
		ArrayList<Byte> toConvertByteArrayList = new ArrayList<Byte>();
		toConvertByteArrayList.addAll(Arrays.asList(toConvertByteArray));
		
		StructInfo structInfo = getStructInfoAnnotation(converted.getClass());
		
		for (Field field : converted.getClass().getDeclaredFields()) {
			Object convertedObject = null;
			if (field.isAnnotationPresent(FixedLengthArray.class)) {
				FixedLengthArray fixedLengthArray = field.getAnnotation(FixedLengthArray.class);
				convertedObject = Array.newInstance(field.getType().getComponentType(), fixedLengthArray.length());
				checkFixedLengthArrayAnnotations(converted.getClass(), convertedObject, field);
				fixedLengthArrayFromByteArray(field, fixedLengthArray, convertedObject, toConvertByteArrayList, structInfo.endianness());
			} else if (field.isAnnotationPresent(VariableLengthArray.class)) {
				
			} else if (field.isAnnotationPresent(SimpleField.class)) {
				convertedObject = simpleFieldFromByteArray(field.getType(), 
						field.getAnnotation(SimpleField.class), toConvertByteArrayList, structInfo.endianness());
				
			} else if (field.isAnnotationPresent(CompositeField.class)) {
				convertedObject = compositeFieldFromByteArray(field.getType(), toConvertByteArrayList);
			}
			invokeSetter(converted, field, convertedObject, field.getType());
		}
		
		Byte[] returnByteArray = new Byte[toConvertByteArrayList.size()];
		returnByteArray = toConvertByteArrayList.toArray(returnByteArray);
		return returnByteArray;
	}
	
	/**
	 * Converte un array di Byte in un Array di oggetti
	 * @param converted
	 * @param field oggetto che descrive il campo da convertire
	 * @param toConvertByteArrayList ArrayList contenente i Bytes da utilizzare per la conversione
	 * @param endianness Formato dei Byte nell'ArrayList
	 * @return Oggetto rappresentante l'array in cui sono stati caricati i dati presenti nell'ArrayList
	 * @throws IllegalArgumentException
	 */
	private static void fixedLengthArrayFromByteArray(Field field, FixedLengthArray fixedLengthArray, Object convertedArray,
			ArrayList<Byte> toConvertByteArrayList, Endianness endianness) throws IllegalArgumentException {
		Object convertedArrayElement;
		if (field.isAnnotationPresent(SimpleField.class)) {
			SimpleField simpleField = field.getAnnotation(SimpleField.class);
			for (int i = 0; i < fixedLengthArray.length(); i ++) {
				convertedArrayElement = simpleFieldFromByteArray(field.getType().getComponentType(), simpleField, toConvertByteArrayList, endianness);
				Array.set(convertedArray, i, convertedArrayElement);
			}
			
		} else if (field.isAnnotationPresent(CompositeField.class)) {
			for (int i = 0; i < fixedLengthArray.length(); i++) {
				convertedArrayElement = compositeFieldFromByteArray(field.getType().getComponentType(), toConvertByteArrayList);
				Array.set(convertedArray, i, convertedArrayElement);
			}
		}
	}
	
	/**
	 * Crea un oggetto di classe typeClass e lo inizializza con i Byte passati come parametro.
	 * I Bytes utilizzati vengono consumati (eliminati dall'ArrayList passata come parametro)
	 * @param typeClass classe dell'oggetto da creare
	 * @param simpleField annotazione @SimpleField contenente le informazioni relative al
	 * @param toConvertByteArrayList ArrayList contenente i Bytes da utilizzare per l'inizializzazione
	 * 		  dell'oggetto da istanziare
	 * @param endianness Formato dei dati nell'ArrayList di Byte
	 * @return Oggetto di tipo typeClass inizializzato a seconda dei dati contenuti nell'annotazione ed
	 * 		   i valori presenti nell'ArrayList di Byte
	 * @throws IllegalArgumentException
	 */
	private static <T> Object simpleFieldFromByteArray(Class<T> typeClass, SimpleField simpleField, 
			ArrayList<Byte> toConvertByteArrayList, Endianness endianness) throws IllegalArgumentException {
		Byte[] byteArray = new Byte[simpleField.size()];
		try {
			byteArray = toConvertByteArrayList.subList(0, simpleField.size()).toArray(byteArray);
		} catch (IndexOutOfBoundsException e) {
			throw new IllegalArgumentException("L'array di byte passato come parametro non contiene abbastanza " +
					"elementi per riempire la struttura richiesta!");
		}
		Object converted = loadNumberFromByteArray(byteArray, typeClass, endianness);
		toConvertByteArrayList.subList(0, simpleField.size()).clear();
		return converted;
	}
	
	/**
	 * Converte un array di Bytes in un oggetto composto
	 * @param typeClass Classe dell'oggetto composto da convertire
	 * @param toConvertByteArrayList ArrayList di Bytes da utilizzare per riempire l'oggetto
	 * @return Oggetto composto in cui sono stati caricati i dati presenti nell'array
	 * @throws IllegalArgumentException nel caso il tipo di oggetto da creare non sia opportunamente annotato
	 */
	private static <T> T compositeFieldFromByteArray(Class<T> typeClass, 
			ArrayList<Byte> toConvertByteArrayList) throws IllegalArgumentException {
		Byte[] toConvertByteArray = new Byte[toConvertByteArrayList.size()];
		toConvertByteArray = toConvertByteArrayList.toArray(toConvertByteArray);
		
		T converted = createNewObjectInstance(typeClass);
		toConvertByteArray = fromByteArray(converted, toConvertByteArray);
		
		toConvertByteArrayList.clear();
		toConvertByteArrayList.addAll(Arrays.asList(toConvertByteArray));
		
		return converted;
	}
	
	/**
	 * Crea un'istanza dell'oggetto richiesto
	 * @param typeClass Class<T> che descrive il tipo di oggetto da creare
	 * @return Istanza dell'oggetto richiesto
	 * @throws IllegalArgumentException nel caso l'oggetto non abbia un costruttore pubblico o sia una classe astratta o un'interfaccia
	 */
	private static <T> T createNewObjectInstance(Class<T> typeClass) throws IllegalArgumentException {
		T objectInstance;
		try {
			objectInstance = typeClass.newInstance();
			return objectInstance;
			
		} catch (InstantiationException e) {
			throw new IllegalArgumentException("Il tipo " + typeClass.getName() + " e' un'interfaccia o una classe astratta e " +
			"non puo' essere istanziato!");
			
		} catch (IllegalAccessException e) {
			throw new IllegalArgumentException("L'oggetto di tipo " + typeClass.getName() + " non dispone di costruttore pubblico!");
		}
	}
	
	/**
	 * Converte un oggetto POJO opportunamente annotato in un array di bytes
	 * @param toConvert Oggetto POJO da convertire
	 * @return Rappresentazione sotto forma di array di Byte dell'oggetto passato come paramentro 
	 * @throws IllegalArgumentException Eccezione lanciata nel caso l'oggetto passato come 
	 * parametro non possa essere convertito
	 */
	public static Byte[] toByteArray(Object toConvert) throws IllegalArgumentException {
		ArrayList<Byte> byteArrayList = new ArrayList<Byte>();
		List<Byte> tmpByteList;
		StructInfo structInfo;
		
		structInfo = getStructInfoAnnotation(toConvert.getClass());
		
		for (Field field : toConvert.getClass().getDeclaredFields()) {
			Object fieldToConvert = invokeGetter(toConvert, field);
			
			if (field.isAnnotationPresent(FixedLengthArray.class)) {
				checkFixedLengthArrayAnnotations(toConvert.getClass(), fieldToConvert, field);
				tmpByteList = fixedLengthArrayToByteArray(toConvert.getClass(), fieldToConvert, field, structInfo.endianness());
				byteArrayList.addAll(tmpByteList);
				
			} else if (field.isAnnotationPresent(VariableLengthArray.class)) {
				
			} else if (field.isAnnotationPresent(SimpleField.class)) {
				SimpleField simpleField = field.getAnnotation(SimpleField.class);
				tmpByteList = simpleFieldToByteArray(fieldToConvert, simpleField.size(), structInfo.endianness());
				if (tmpByteList == null) {
					throw new IllegalArgumentException("Il campo " + field.getName() + " dell'oggetto " 
							+ toConvert.getClass().getName() + " non e' un tipo semplice convertibile!");
				}
				byteArrayList.addAll(tmpByteList);
				
			} else if (field.isAnnotationPresent(CompositeField.class)) {
				byteArrayList.addAll(compositeFieldToByteArray(fieldToConvert));
			}
		}
		
		Byte[] byteArray = new Byte[byteArrayList.size()];
		return byteArrayList.toArray(byteArray);
	}
	
	/**
	 * Restituisce, se presente, l'annotazione StructInfo associata alla classe passata come parametro
	 * @param typeClass Classe da esaminare
	 * @return Annotazione StructInfo relativa alla classe passata come parametro
	 * @throws IllegalArgumentException nel caso la classe non contenga l'annotazione richiesta
	 */
	private static <T> StructInfo getStructInfoAnnotation(Class<T> typeClass) throws IllegalArgumentException {
		if (!typeClass.isAnnotationPresent(StructInfo.class)) {
			throw new IllegalArgumentException("Il tipo " + typeClass.getName()
					+ " passato come parametro non contiene l'annotazione " +
							"StructInfo necessaria per la conversione!");
		}
		return typeClass.getAnnotation(StructInfo.class);
	}
	
	/**
	 * Converte un array di dimensione fissa in una List di byte
	 * @param parentObjetClass classe dell'oggetto che contiene l'array da convertire
	 * @param toConvert oggetto da convertire (array)
	 * @param field campo corrispondente all'oggetto da convertire
	 * @param endianness ordine dei byte richiesto
	 * @return List contenente la rappresentazione in Byte dell'array 
	 * @throws IllegalArgumentException
	 */
	private static List<Byte> fixedLengthArrayToByteArray(Class<?> parentObjetClass, Object toConvert, Field field, Endianness endianness) throws IllegalArgumentException {
		List<Byte> convertedBytesArrayList = new ArrayList<Byte>();
		List<Byte> tmpByteList;
		FixedLengthArray fixedLengthArray = field.getAnnotation(FixedLengthArray.class);
		int elementSize = 0; 
		int paddingLength;
		
		if (field.isAnnotationPresent(SimpleField.class)) {
			SimpleField simpleField = field.getAnnotation(SimpleField.class);
			elementSize = simpleField.size();
			
			for (int i = 0; i < Array.getLength(toConvert); i++) {
				tmpByteList = simpleFieldToByteArray(Array.get(toConvert, i), elementSize, endianness);
				if (tmpByteList == null) {
					throw new IllegalArgumentException("Il campo " + field.getName() + " dell'oggetto " + parentObjetClass.getName() +
					" non e' un tipo semplice convertibile!");
				}
				convertedBytesArrayList.addAll(tmpByteList);
			}
			
		} else if (field.isAnnotationPresent(CompositeField.class)) {
			CompositeField compositeField = field.getAnnotation(CompositeField.class);
			elementSize = compositeField.size();
			
			if (elementSize < 0) {
				throw new IllegalArgumentException("Il campo " + field.getName() + " dell'oggetto" + parentObjetClass.getName() +
						" e' dichiarato come FixedLengthArray ma l'annotazione degli elementi che lo costituiscono " +
						"manca dell'elemento size!");
			}
			
			for (int i = 0; i < Array.getLength(toConvert); i++) {
				convertedBytesArrayList.addAll(Arrays.asList(toByteArray(Array.get(toConvert, i))));
			}
		}
		
		paddingLength = (fixedLengthArray.length() - Array.getLength(toConvert)) * elementSize;
		if (paddingLength != 0) {
			convertedBytesArrayList = addPadding(convertedBytesArrayList, paddingLength, fixedLengthArray.padding(), fixedLengthArray.paddingPosition());
		}
		
		return convertedBytesArrayList;
	}
	
	/**
	 * Aggiunge i Bytes di padding
	 * @param originalList Lista originale a cui aggiungere il padding
	 * @param paddingSize Dimensione (in Bytes) del padding
	 * @param padding Byte da utilizzare come padding
	 * @param paddingPosition Posizione del padding
	 * @return Lista contenente con padding aggiunto
	 */
	private static List<Byte> addPadding(List<Byte> originalList, int paddingSize, Byte padding, PaddingPosition paddingPosition) {
		List<Byte> paddingList = new ArrayList<Byte>(paddingSize);
		paddingList.addAll(Collections.nCopies(paddingSize, padding));
		
		if (paddingPosition == PaddingPosition.AT_END) {
			originalList.addAll(paddingList);
		} else {
			paddingList.addAll(originalList);
			originalList = paddingList;
		}
		
		return originalList;
	}
	
	/**
	 * Controlla che il campo dichiarato come FixedLengthArray abbia le annotazioni necessarie per la conversione
	 * e che le dimensioni massime siano rispettate
	 * @param parentObjectClass Classe dell'oggetto che contiene il campo
	 * @param field Campo da controllare
	 * @throws IllegalArgumentException Eccezione lanciata nel caso il campo non abbia le annotazioni richieste
	 */
	private static void checkFixedLengthArrayAnnotations(Class<?> parentObjectClass, Object toConvert, Field field) throws IllegalArgumentException {
		if (!toConvert.getClass().isArray()) {
			throw new IllegalArgumentException("Il campo " + field.getName() + " dell'oggetto " + parentObjectClass.getName() +
					" e' stato annotato come FixedLengthArray ma non e' un array!");
		}
		if (field.isAnnotationPresent(SimpleField.class) && field.isAnnotationPresent(CompositeField.class)) {
			throw new IllegalArgumentException("L'oggetto di tipo " + parentObjectClass.getName() + 
					" passato come parametro contiene annotazioni errate" +
					" per il metodo di tipo " + field.getName() + " (eliminare @SimpleField o @CompositeField)!");
			
		} else if (!field.isAnnotationPresent(SimpleField.class) && !field.isAnnotationPresent(CompositeField.class)) {
			throw new IllegalArgumentException("L'oggetto di tipo " + parentObjectClass.getClass().getName() +
					" passato come parametro contiene annotazioni errate" +
					" per il metodo di tipo " + field.getName() + " (aggiungere @SimpleField o @CompositeField)!");
			
		} else if (Array.getLength(toConvert) > ((FixedLengthArray)field.getAnnotation(FixedLengthArray.class)).length()) {
			throw new IllegalArgumentException("Il campo " + field.getName() + " dell'oggetto " + parentObjectClass.getName() +
					" annotato come FixedLengthArray contiene un array di dimensioni " +
					"maggiori rispetto a quelle dichiarate nell'annotazione!");
			
		} else if (field.isAnnotationPresent(CompositeField.class)) {
			CompositeField compositeField = field.getAnnotation(CompositeField.class);
			if (compositeField.size() == -1) {
				throw new IllegalArgumentException("Il campo " + field.getName() + " dell'oggetto" + parentObjectClass.getName() +
						" e' dichiarato come FixedLengthArray ma l'annotazione degli elementi che lo costituiscono " +
						"manca dell'elemento size!");
			}
		}
	}
	
	/**
	 * 
	 * @param toConvert
	 * @param size
	 * @param endianness
	 * @return
	 */
	private static List<Byte> simpleFieldToByteArray(Object toConvert, int size, Endianness endianness) {
		return storeNumberToByteArray(toConvert, endianness, size);
	}
	
	/**
	 * 
	 * @param toConvert
	 * @return
	 */
	private static List<Byte> compositeFieldToByteArray(Object toConvert) {
		return Arrays.asList(toByteArray(toConvert));
	}
		
	/**
	 * Converte il l'oggetto passato come parametro in un ArrayList<Byte>
	 * Il metodo restituisce null nel caso il tipo di oggetto da convertire non sia supportato.
	 * Il numero sara' troncato nel caso la sua rappresentazione necessiti piu' bytes
	 * di quanti specificati con il parametro size.
	 * @param toConvert numero da convertire
	 * @param endianness ordine dei byte nell'array
	 * @param size numero di bytes da utilizzare per la conversione
	 * @return ArrayList<Byte> rappresentante il numero convertito
	 */
	private static ArrayList<Byte> storeNumberToByteArray(Object toConvert, Endianness endianness, int size) {
		ArrayList<Byte> byteArray = null;
		
		if (toConvert instanceof Number) {
			BigInteger number = new BigInteger(toConvert.toString());
			ArrayList<Byte> tmpByteArray = new ArrayList<Byte>();
			
			tmpByteArray.addAll(Arrays.asList(ArrayUtility.toByteClassArray(number.toByteArray())));
			int excessBytes = size - tmpByteArray.size();
			
			if (excessBytes > 0) {
				byteArray = new ArrayList<Byte>(size);
				if (number.compareTo(BigInteger.ZERO) < 0) {
					byteArray.addAll(Collections.nCopies(excessBytes, new Byte("-1")));
				} else {
					byteArray.addAll(Collections.nCopies(excessBytes, new Byte("0")));
				}		
				byteArray.addAll(tmpByteArray);		
			} else if (excessBytes < 0) {
				byteArray = new ArrayList<Byte>(size);
				byteArray.addAll(tmpByteArray.subList(-1 * excessBytes, tmpByteArray.size()));
			} else {
				byteArray = tmpByteArray; 
			}
		}
		
		if (byteArray != null && endianness == Endianness.LITTLE_ENDIAN) {
			Collections.reverse(byteArray);
		}
		
		return byteArray;
	}
	
	/**
	 * Converte un array di Byte in un numero
	 * @param byteArray array di byte da convertire
	 * @param typeClass Classe del tipo in cui l'array di Byte dovra' essere caricato
	 * @param endianness Formato in cui i dati sono stati memorizzati nell'array
	 * @return Numero inizializzato secondo il valore contenuto nell'array di Byte
	 */
	private static <T> Object loadNumberFromByteArray(Byte[] byteArray, Class<T> typeClass, Endianness endianness) {
		BigInteger number;
		
		if (endianness == Endianness.LITTLE_ENDIAN) {
			byteArray = ArrayUtility.reverseArray(byteArray);
		}
		number = new BigInteger(ArrayUtility.toPrimitiveByteArray(byteArray));
		
		return convertToSpecificNumber(number, typeClass);
	}
	
	/**
	 * Converte un numero BigInteger in un determinato tipo. Nel caso il tipo di destinazione
	 * non sia bbastanza grande per contenere il numero si verifichera' una perdita di dati
	 * @param toConvert BigInteger rappresentante il numero da convertire nel tipo adatto
	 * @param typeClass classe in cui si vuole che il numero sia convertito
	 * @return numero rappresentato con la classe richiesta
	 */
	private static <T> Object convertToSpecificNumber(BigInteger toConvert, Class<T> typeClass) {
		Object toReturn = null;
		
		if (typeClass == Byte.class || typeClass == byte.class) {
			toReturn = toConvert.byteValue();
		} else if (typeClass == Short.class || typeClass == short.class) {
			toReturn = toConvert.shortValue();
		} else if (typeClass == Integer.class || typeClass == int.class) {
			toReturn = toConvert.intValue();
		} else if (typeClass == Long.class || typeClass == long.class) {
			toReturn = toConvert.longValue();
		} else if (typeClass == BigInteger.class) {
			toReturn = toConvert;
		}
		
		return toReturn;
	}

	/**
	 * Invoca il metodo getter corrispondente al campo in esame
	 * @param object oggetto su cui invocare il metodo
	 * @param field campo da ricavare tramite getter
	 * @return oggetto restituito dal metodo getter
	 * @throws IllegalArgumentException
	 */
	private static Object invokeGetter(Object object, Field field) throws IllegalArgumentException {
		Object tmpObject;
		
		try {
			String methodName = "get" + Character.toUpperCase(field.getName().charAt(0)) + field.getName().substring(1);
			Method tmpMethod = object.getClass().getMethod(methodName, (Class[]) null);
			
			tmpObject = tmpMethod.invoke(object, (Object[]) null);
			
			if (tmpObject == null) {
				throw new IllegalArgumentException("Il campo " + field.getName() + " dell'oggetto " + object.getClass().getName() +
						" e' null e pertanto non puo' essere convertito!");
			}
			
			return tmpObject;

		} catch (SecurityException e) {
			throw new IllegalArgumentException("L'oggetto di tipo " + object.getClass().getName()
					+ " passato come parametro non permette l'accesso al campo"
					+ field.getName() + ". Controllare che sia stato dichiarato come pubblico!");

		} catch (NoSuchMethodException e) {
			throw new IllegalArgumentException("L'oggetto di tipo " + object.getClass().getName()
					+ " passato come parametro non ha metodo getter per il campo "+ field.getName() + "!");

		} catch (IllegalAccessException e) {
			throw new IllegalArgumentException("L'oggetto di tipo " + object.getClass().getName() 
					+ " passato come parametro non ha getter pubblico per il campo" + field.getName() + "!");
			
		} catch (InvocationTargetException e) {
			throw new IllegalArgumentException("Il metodo getter per il campo"
					+ field.getName() + " del tipo " + object.getClass().getName() + 
			"ha generato un'eccezione! Controllare la sua implementazione");
		}
	}
	
	/**
	 * Invoca il metodo setter corrispondente al campo in esame
	 * @param object oggetto su cui invocare il metodo
	 * @param field campo da impostare tramite setter
	 * @param valueToSet valore a cui impostare il campo
	 * @throws IllegalArgumentException
	 */
	private static <T> void invokeSetter(Object object, Field field, Object valueToSet, Class<T> valueToSetClass) throws IllegalArgumentException {

		try {	
			String methodName = "set" + Character.toUpperCase(field.getName().charAt(0)) + field.getName().substring(1);
			Class<?>[] parameterClasses = new Class<?>[1];
			parameterClasses[0] = valueToSetClass;
			Method tmpMethod = object.getClass().getMethod(methodName, parameterClasses);

			Object[] tmpArray = new Object[1];
			tmpArray[0] = valueToSet;
			tmpMethod.invoke(object, tmpArray);

		} catch (SecurityException e) {
			throw new IllegalArgumentException("L'oggetto di tipo " + object.getClass().getName()
					+ " passato come parametro non permette l'accesso al campo"
					+ field.getName() + ". Controllare che sia stato dichiarato come pubblico!");

		} catch (NoSuchMethodException e) {
			throw new IllegalArgumentException("L'oggetto di tipo " + object.getClass().getName()
					+ " passato come parametro non ha metodo setter per il campo "+ field.getName() + "!");

		} catch (IllegalAccessException e) {
			throw new IllegalArgumentException("L'oggetto di tipo " + object.getClass().getName() 
					+ " passato come parametro non ha setter pubblico per il campo" + field.getName() + "!");

		} catch (InvocationTargetException e) {
			throw new IllegalArgumentException("Il metodo setter per il campo"
					+ field.getName() + " del tipo " + object.getClass().getName() + 
			"ha generato un'eccezione! Controllare la sua implementazione");
		}
	}
}